Skip to content

fix: resolve skills-core.js path using home directory for Windows compatibility#326

Closed
kr1json wants to merge 1 commit intoobra:mainfrom
kr1json:fix/windows-plugin-import-path
Closed

fix: resolve skills-core.js path using home directory for Windows compatibility#326
kr1json wants to merge 1 commit intoobra:mainfrom
kr1json:fix/windows-plugin-import-path

Conversation

@kr1json
Copy link

@kr1json kr1json commented Jan 22, 2026

Summary

Fixes #303

Resolves the "Cannot find module '../../lib/skills-core.js'" error on Windows by using absolute paths instead of relative imports.

Motivation and Context

On Windows, when using ln -sf in Git Bash (which behaves like cp instead of creating a symlink), the plugin file is copied rather than symlinked. This causes relative import paths to resolve incorrectly:

Problem 1 - skills-core.js import:

  • Plugin location: ~/.config/opencode/plugin/superpowers.js
  • Relative path ../../lib/skills-core.js resolves to: ~/.config/lib/skills-core.js
  • Correct location: ~/.config/opencode/superpowers/lib/skills-core.js

Problem 2 - skills directory:

  • Relative path ../../skills resolves to: ~/.config/skills/
  • Correct location: ~/.config/opencode/superpowers/skills/

Error messages:

ResolveError: "C:\Users\logix\.config\lib\skills-core.js" failed to resolve
현재 설치된 스킬이 없습니다. 슈퍼파워 스킬을 C:\Users\logix\.config\skills\에 설치하거나...

This prevents the OpenCode plugin from loading entirely on Windows.

How Has This Been Tested?

  • ✅ Tested on Windows 11 with OpenCode
  • ✅ Verified plugin loads without errors after the fix
  • ✅ Confirmed find_skills tool returns correct skill list
  • ✅ Confirmed use_skill tool works correctly
  • ✅ Works with both copied file (Git Bash ln -sf) and manual symlink scenarios
  • ✅ Cross-platform compatible: uses os.homedir(), path.join(), and pathToFileURL() which work on Mac/Linux/Windows

Code Changes

Before fix:

import * as skillsCore from '../../lib/skills-core.js';
// Fails on Windows: wrong path resolution

const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
// Fails on Windows: wrong path resolution

After fix:

import { pathToFileURL } from 'url';

const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
// Works on all platforms

const superpowersSkillsDir = path.join(superpowersRoot, 'skills');
// Works on all platforms

Key changes:

  1. Use os.homedir() to get platform-independent home directory
  2. Construct absolute path to superpowers installation
  3. Use pathToFileURL() to convert file path to proper URL for dynamic import (required on Windows)
  4. Apply same fix to skills directory path

Breaking Changes

None. This is a bug fix that maintains backward compatibility with Unix systems while adding Windows support.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

This issue affects all Windows users trying to install superpowers via the documented installation instructions. The fix uses platform-independent Node.js APIs:

  • os.homedir() - Returns correct home directory on all platforms
  • path.join() - Handles path separators correctly (/ vs )
  • pathToFileURL() - Converts absolute paths to file:// URLs for dynamic import

The solution works correctly regardless of:

  • Whether the file is symlinked or copied
  • The user's home directory location
  • The shell being used (Git Bash, PowerShell, WSL, etc.)

The change is minimal (11 lines added, 4 removed) and focused solely on the import resolution issue.

@kr1json kr1json force-pushed the fix/windows-plugin-import-path branch from 1a0d0b3 to 492bf99 Compare January 22, 2026 14:29
@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Replaces a static import of skills-core with a top-level async runtime import that resolves the module path from a user-specific Superpowers installation directory, using environment-driven and platform-aware path resolution before loading and exposing the same helper functions.

Changes

Cohort / File(s) Summary
Dynamic Module Resolution
.opencode/plugin/superpowers.js
Replaces require('../../lib/skills-core.js') with a top-level await import(...) using a resolved absolute path. Adds pathToFileURL usage, home/OPENCODE root discovery, OS-aware path handling, and runtime variable resolvedSkillsCore providing resolveSkillPath, stripFrontmatter, extractFrontmatter, findSkillsInDir. (+11 / -4)

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Plugin as "Plugin (.opencode/plugin/superpowers.js)"
  participant Env as "Environment / OS"
  participant FS as "Filesystem"
  participant SkillsCore as "skills-core (dynamic module)"
  Plugin->>Env: read env vars (e.g., OPENCODE_CONFIG_DIR, HOME)
  Env-->>Plugin: resolved superpowersRoot / paths
  Plugin->>FS: compute absolute skillsCore path + pathToFileURL
  FS-->>Plugin: file URL for import
  Plugin->>SkillsCore: dynamic import(fileURL)
  SkillsCore-->>Plugin: exported helpers (resolveSkillPath, stripFrontmatter, ...)
  Plugin->>Plugin: initialize using resolvedSkillsCore functions
Loading

(Note: colored rectangles not required for this simple flow.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped from static file to free,
A runtime path beneath my tree,
Env and OS I sniffed with care,
Then imported magic from over there,
Now skills unfold with nimble flair. 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing the skills-core.js import path resolution to use the home directory for Windows compatibility, which directly addresses the PR's primary objective of resolving issue #303.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.opencode/plugin/superpowers.js (1)

42-43: superpowersSkillsDir still uses __dirname-relative path.

The PR fixes the skills-core.js import by using a hardcoded home-directory path, but superpowersSkillsDir still derives from __dirname with ../../skills. If the plugin file is copied (the same Windows scenario the PR aims to fix), this path will resolve incorrectly (e.g., ~/.config/opencode/plugin/../../skills~/.config/skills).

Consider using the same superpowersRoot for consistency:

🛠️ Suggested fix: use superpowersRoot for skills directory
   const homeDir = os.homedir();
   const projectSkillsDir = path.join(directory, '.opencode/skills');
-  // Derive superpowers skills dir from plugin location (works for both symlinked and local installs)
-  const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
+  // Use the same superpowersRoot for skills directory (works for both symlinked and copied installs)
+  const superpowersSkillsDir = path.join(superpowersRoot, 'skills');
🤖 Fix all issues with AI agents
In @.opencode/plugin/superpowers.js:
- Around line 16-22: Wrap the dynamic top-level import of skills-core.js in a
try-catch around the import(pathToFileURL(skillsCorePathResolved).href) call:
catch the error, log a clear, actionable message that includes
skillsCorePathResolved and the caught error (use existing logger or
console.error), and either rethrow or gracefully disable the plugin depending on
desired behavior; ensure this handling is applied where homeDir,
superpowersRoot, skillsCorePathResolved and skillsCore are defined so the plugin
fails with a helpful message instead of an unhandled exception.
- Around line 19-22: The code hardcodes superpowersRoot and loads skills-core.js
from ~/.config/opencode/superpowers which ignores OPENCODE_CONFIG_DIR; update
the resolution to reuse the same config-dir logic used later (the
OPENCODE_CONFIG_DIR fallback) so that superpowersRoot is derived from that
resolved config directory, then rebuild skillsCorePathResolved and import
skillsCore from that path; ensure you reference the same environment variable
handling used elsewhere so loading respects OPENCODE_CONFIG_DIR.
🧹 Nitpick comments (1)
.opencode/plugin/superpowers.js (1)

19-20: Duplicate homeDir declaration.

homeDir is declared at module scope (line 19) and again inside SuperpowersPlugin (line 40). The inner declaration shadows the outer one and both call os.homedir(). Consider removing the duplicate and reusing the module-level constant.

♻️ Suggested refactor
 export const SuperpowersPlugin = async ({ client, directory }) => {
-  const homeDir = os.homedir();
   const projectSkillsDir = path.join(directory, '.opencode/skills');

Also applies to: 40-40

Comment on lines +16 to +22
// Resolve skills-core.js path dynamically to work with both symlinks and copies
// On Mac/Linux with symlink: __dirname points to actual location
// On Windows with copy: we need to find the superpowers installation
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling for dynamic import.

The top-level await import() will throw an unhandled error if skills-core.js doesn't exist at the resolved path (e.g., fresh install, non-standard installation). This causes the entire plugin to fail to load with an unhelpful error message.

🛠️ Suggested fix: wrap in try-catch with helpful error
 // Resolve skills-core.js path dynamically to work with both symlinks and copies
 // On Mac/Linux with symlink: __dirname points to actual location
 // On Windows with copy: we need to find the superpowers installation
 const homeDir = os.homedir();
 const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
 const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
-const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
+
+let skillsCore;
+try {
+  skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
+} catch (err) {
+  throw new Error(
+    `Failed to load skills-core.js from ${skillsCorePathResolved}. ` +
+    `Ensure superpowers is installed at ${superpowersRoot}. ` +
+    `Original error: ${err.message}`
+  );
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Resolve skills-core.js path dynamically to work with both symlinks and copies
// On Mac/Linux with symlink: __dirname points to actual location
// On Windows with copy: we need to find the superpowers installation
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
// Resolve skills-core.js path dynamically to work with both symlinks and copies
// On Mac/Linux with symlink: __dirname points to actual location
// On Windows with copy: we need to find the superpowers installation
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
let skillsCore;
try {
skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
} catch (err) {
throw new Error(
`Failed to load skills-core.js from ${skillsCorePathResolved}. ` +
`Ensure superpowers is installed at ${superpowersRoot}. ` +
`Original error: ${err.message}`
);
}
🤖 Prompt for AI Agents
In @.opencode/plugin/superpowers.js around lines 16 - 22, Wrap the dynamic
top-level import of skills-core.js in a try-catch around the
import(pathToFileURL(skillsCorePathResolved).href) call: catch the error, log a
clear, actionable message that includes skillsCorePathResolved and the caught
error (use existing logger or console.error), and either rethrow or gracefully
disable the plugin depending on desired behavior; ensure this handling is
applied where homeDir, superpowersRoot, skillsCorePathResolved and skillsCore
are defined so the plugin fails with a helpful message instead of an unhandled
exception.

Comment on lines +19 to +22
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistency: hardcoded path ignores OPENCODE_CONFIG_DIR environment variable.

The superpowersRoot path is hardcoded to ~/.config/opencode/superpowers, but later (lines 45-46) the plugin respects OPENCODE_CONFIG_DIR for the config directory. If a user sets this env var, skills-core.js will still be loaded from the default location, causing a mismatch.

Consider using the same resolution logic:

🛠️ Suggested fix: respect OPENCODE_CONFIG_DIR
+// Helper to normalize path (needed before plugin init)
+const normalizePathEarly = (p, home) => {
+  if (!p || typeof p !== 'string') return null;
+  let normalized = p.trim();
+  if (!normalized) return null;
+  if (normalized.startsWith('~/')) {
+    normalized = path.join(home, normalized.slice(2));
+  } else if (normalized === '~') {
+    normalized = home;
+  }
+  return path.resolve(normalized);
+};
+
 const homeDir = os.homedir();
-const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
+const envConfigDir = normalizePathEarly(process.env.OPENCODE_CONFIG_DIR, homeDir);
+const superpowersRoot = path.join(envConfigDir || path.join(homeDir, '.config/opencode'), 'superpowers');
 const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
// Helper to normalize path (needed before plugin init)
const normalizePathEarly = (p, home) => {
if (!p || typeof p !== 'string') return null;
let normalized = p.trim();
if (!normalized) return null;
if (normalized.startsWith('~/')) {
normalized = path.join(home, normalized.slice(2));
} else if (normalized === '~') {
normalized = home;
}
return path.resolve(normalized);
};
const homeDir = os.homedir();
const envConfigDir = normalizePathEarly(process.env.OPENCODE_CONFIG_DIR, homeDir);
const superpowersRoot = path.join(envConfigDir || path.join(homeDir, '.config/opencode'), 'superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
🤖 Prompt for AI Agents
In @.opencode/plugin/superpowers.js around lines 19 - 22, The code hardcodes
superpowersRoot and loads skills-core.js from ~/.config/opencode/superpowers
which ignores OPENCODE_CONFIG_DIR; update the resolution to reuse the same
config-dir logic used later (the OPENCODE_CONFIG_DIR fallback) so that
superpowersRoot is derived from that resolved config directory, then rebuild
skillsCorePathResolved and import skillsCore from that path; ensure you
reference the same environment variable handling used elsewhere so loading
respects OPENCODE_CONFIG_DIR.

…patibility

Fixes obra#303

On Windows, when using ln -sf (which behaves like cp in Git Bash),
the relative import path '../../lib/skills-core.js' fails because
it resolves from ~/.config/opencode/plugin/ to an incorrect location.

This change uses os.homedir() to construct an absolute path to the
superpowers installation directory, then locates skills-core.js from there.
This works correctly on all platforms regardless of whether the plugin
file is symlinked or copied.

Tested on:
- Windows 11 with OpenCode (copied file via Git Bash ln -sf)
- Works on Mac/Linux with symlinks (cross-platform path.join)
@obra
Copy link
Owner

obra commented Jan 22, 2026

Analysis

This PR attempts to fix #303 / #232 (Windows module resolution failure) by hardcoding the superpowers installation path.

Problem with this approach

The fix changes:

// Before: relative import (works with proper symlink)
import * as skillsCore from '../../lib/skills-core.js';
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');

// After: hardcoded path (only works with specific install location)
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCore = await import(pathToFileURL(path.join(superpowersRoot, 'lib/skills-core.js')).href);
const superpowersSkillsDir = path.join(superpowersRoot, 'skills');

This breaks flexibility:

  1. ❌ Breaks project-local installations (.opencode/plugin/)
  2. ❌ Breaks custom install locations
  3. ❌ Assumes everyone clones to ~/.config/opencode/superpowers

Root Cause

The real problem isn't the code - it's that Git Bash ln -sf on Windows copies the file instead of creating a symlink. When the file is copied, relative imports resolve incorrectly.

Better Solutions

  1. Fix the installation docs for Windows - Use mklink /J (directory junction) or proper symlinks
  2. Clone the plugin directly - Don't symlink, clone the whole repo to the plugin location
  3. Detect symlink vs copy - Try relative import, fall back to well-known location on failure

Tracking Issue

See #232 for the canonical tracking of this problem.

I recommend not merging this PR as-is because it trades one problem (Windows install broken) for another (all non-standard installs broken).

@obra
Copy link
Owner

obra commented Jan 22, 2026

Closing in favor of PR #330. That PR fixes the Windows issue by:

  1. Removing the skills-core.js dependency entirely
  2. Providing proper Windows installation docs with correct symlink/junction commands

This avoids the hardcoded path approach which would have broken project-local and custom installations.

@obra obra closed this Jan 22, 2026
@kr1json kr1json deleted the fix/windows-plugin-import-path branch January 23, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[opencode] Windows 11 Git Bash Install

2 participants

Comments